const std = @import("std");
const curses = @cImport({
	@cInclude("ncurses.h");
});

const CellChar = @import("constants.zig").CellChar;
const CellCharType = @import("constants.zig").CellCharType;
const CellColor = @import("constants.zig").CellColor;
const Grid = @import("grid.zig").Grid;
const Level = @import("level.zig").Level;
const Pipe = @import("pipe.zig").Pipe;
const Pipes = @import("pipes.zig").Pipes;
const PipeType = @import("pipe.zig").PipeType;
const Vector = @import("vector.zig").Vector;


pub const Game = struct {
	level_path: []const u8,
	window: *curses.struct__win_st,
	allocator: std.mem.Allocator = std.heap.page_allocator,
	_level: Level = undefined,
	_grid: Grid(CellCharType) = undefined,
	_selected: ?*Pipe = null,
	_cursor: Vector = Vector{.x = 0, .y = 0},


	pub fn init(self: *Game) !void {
		self._level = Level{};
		try self._level.init(self.level_path);

		self._grid = Grid(u16){
			.width = self._level.get_grid_size().x,
			.height = self._level.get_grid_size().y,
			.empty_char = @intFromEnum(CellChar.EMPTY),
			.empty_color_pair_id = @intFromEnum(CellColor.NORMAL),
			.window = self.window,
		};
		try self._grid.init();
	}


	pub fn deinit(self: *Game) void {
		self._level.deinit();
		self._grid.deinit();
	}


	pub fn main_loop(self: *Game) !void {
		_ = curses.clear();
		while (true) {
			try self.step();
			if (try self._level.is_completed()) {
				try self.fill_pipes();
				Game.game_won();
				break;
			}
			std.time.sleep(std.time.ns_per_s / 10);
		}
	}


	fn step(self: *Game) !void {
		try self.apply_input();

		self._grid.clear();
		self.set_grid_cells(true);
		try self._grid.display();
	}


	fn apply_input(self: *Game) !void {
		const key: c_int = Game.get_input();
		switch (key) {
			10, 32 => self.toggle_selection(),
			65 => try self.move_up(),
			66 => try self.move_down(),
			67 => try self.move_right(),
			68 => try self.move_left(),
			else => {},
		}
	}


	fn get_input() c_int {
		var key: c_int = curses.getch();
		if (key == 10 or key == 32) {
			_ = curses.flushinp();
			return key;
		}

		_ = curses.getch();
		key = curses.getch();
		_ = curses.flushinp();

		return key;
	}


	fn toggle_selection(self: *Game) void {
		if (self._selected != null) {
			self._selected = null;
		} else {
			var pipe: ?*Pipe = self._level.get_in_cell(
					self._cursor.x, self._cursor.y);
			if (pipe != null and pipe.?.is_end()) {
				pipe = null;
			}
			self._selected = pipe;
		}
	}


	fn move_up(self: *Game) !void {
		if (self._selected) |pipe| {
			if (self._level.get_in_cell(pipe.x, pipe.y -| 1) == null) {
				self._selected = try self._level.move_pipe(
						pipe, pipe.x, pipe.y -| 1);
				self._cursor.y -|= 1;
			}
		} else {
			self._cursor.y -|= 1;
		}
	}


	fn move_down(self: *Game) !void {
		if (self._selected) |pipe| {
			if (self._level.get_in_cell(pipe.x, pipe.y +| 1) == null) {
				self._selected = try self._level.move_pipe(
						pipe, pipe.x, pipe.y +| 1);
				self._cursor.y +|= 1;
			}
		} else {
			self._cursor.y +|= 1;
		}
	}


	fn move_right(self: *Game) !void {
		if (self._selected) |pipe| {
			if (self._level.get_in_cell(pipe.x +| 1, pipe.y) == null) {
				self._selected = try self._level.move_pipe(
						pipe, pipe.x +| 1, pipe.y);
				self._cursor.x +|= 1;
			}
		} else {
			self._cursor.x +|= 1;
		}
	}


	fn move_left(self: *Game) !void {
		if (self._selected) |pipe| {
			if (self._level.get_in_cell(pipe.x -| 1, pipe.y) == null) {
				self._selected = try self._level.move_pipe(
						pipe, pipe.x -| 1, pipe.y);
				self._cursor.x -|= 1;
			}
		} else {
			self._cursor.x -|= 1;
		}
	}


	fn set_grid_cells(self: *Game, show_cursor: bool) void {
		if (show_cursor) {
			self._grid.set_cell(self._cursor.x, self._cursor.y,
				@intFromEnum(CellChar.EMPTY), @intFromEnum(CellColor.HOVERED));
		}

		var pipe_iter = self._level.pipes_iterator();
		while (pipe_iter.next()) |entry| {
			const pipe: *Pipe = entry.value_ptr;
			const char: CellCharType = Game.get_pipe_char(pipe);
			var color: CellColor = if (pipe.is_end()) CellColor.PIPE_END
					else CellColor.PIPE_NORMAL;

			if (show_cursor) {
				if (pipe == self._selected) {
					color = CellColor.PIPE_SELECTED;
				} else if (
						pipe.x == self._cursor.x
						and pipe.y == self._cursor.y
				) {
					color = CellColor.PIPE_HOVERED;
				}
			}

			self._grid.set_cell(pipe.x, pipe.y, char, @intFromEnum(color));
		}
	}


	fn set_grid_filled_pipes(self: *Game, pipes: *std.AutoHashMap(*Pipe, void))
			void {
		var pipe_iter = pipes.keyIterator();
		while (pipe_iter.next()) |item| {
			const pipe: *Pipe = item.*;
			self._grid.set_cell(
				pipe.x,
				pipe.y,
				Game.get_pipe_char(pipe),
				@intFromEnum(CellColor.PIPE_FILLED)
			);
		}
	}


	fn get_pipe_char(pipe: *Pipe) CellCharType {
		return switch(pipe.type) {
			PipeType.HORIZONTAL => @intFromEnum(CellChar.PIPE_HORIZ),
			PipeType.VERTICAL => @intFromEnum(CellChar.PIPE_VERT),
			PipeType.CORNER_1 => @intFromEnum(CellChar.PIPE_CORNER_1),
			PipeType.CORNER_2 => @intFromEnum(CellChar.PIPE_CORNER_2),
			PipeType.CORNER_3 => @intFromEnum(CellChar.PIPE_CORNER_3),
			PipeType.CORNER_4 => @intFromEnum(CellChar.PIPE_CORNER_4),
			PipeType.END_1 => @intFromEnum(CellChar.PIPE_END_1),
			PipeType.END_2 => @intFromEnum(CellChar.PIPE_END_2),
			PipeType.END_3 => @intFromEnum(CellChar.PIPE_END_3),
			PipeType.END_4 => @intFromEnum(CellChar.PIPE_END_4),
		};
	}


	fn fill_pipes(self: *Game) !void {
		var checked_pipes = std.AutoHashMap(*Pipe, void).init(self.allocator);
		defer checked_pipes.deinit();
		var pipes_to_check = std.PriorityQueue(*Pipe, void, Pipes.pipe_cmp)
				.init(self.allocator, {});
		defer pipes_to_check.deinit();

		try pipes_to_check.add(self._level.get_start_end_pipe());

		var finished = false;
		while (!finished) {
			finished = try self._level.step_through_line(
				&pipes_to_check,
				&checked_pipes
			);

			self._grid.clear();
			self.set_grid_cells(false);
			self.set_grid_filled_pipes(&checked_pipes);
			try self._grid.display();

			std.time.sleep(std.time.ns_per_s / 4);
		}
	}


	fn game_won() void {
		curses.timeout(-1);

		_ = curses.clear();
		_ = curses.attron(curses.COLOR_PAIR(1));
		_ = curses.addstr("\tYOU WIN\n\rPress ENTER to continue");

		var key: c_int = curses.getch();
		while (key != 10) {
			key = curses.getch();
		}

		curses.timeout(0);
	}


	fn game_over() void {
		curses.timeout(-1);

		_ = curses.clear();
		_ = curses.attron(curses.COLOR_PAIR(1));
		_ = curses.addstr("\tGAME OVER\n\rPress ENTER to continue");

		var key: c_int = curses.getch();
		while (key != 10) {
			key = curses.getch();
		}

		curses.timeout(0);
	}
};
